import { truthy } from '@gnlab/utils'
import { useStateWithRef, useRefValue } from '@gnlab/utils/env-react'
import { useObservable } from '@gnlab/utils/env-react/rx'
import { type RefObject } from 'react'
import { Subject } from 'rxjs'
import { type AtMember, type ViewData } from '.'

export interface AtExportItem {
  userId: string
  nickname: string
}

/**
 * 外部可调用此方法来触发 @ 行为
 */
const atEvent = new Subject<AtMember>()
export function triggerAt(member: AtMember) {
  atEvent.next(member)
}

/**
 * 提交、记录 @ 结果
 */
export function useAtList(params: {
  viewData: ViewData | null
  inputRef: RefObject<HTMLElement>
  nodeClassName: string
  setAtList: (list: AtExportItem[]) => void
  closeView: () => void
}) {
  const { viewData, inputRef, nodeClassName, setAtList, closeView } = params
  const viewDataRef = useRefValue(viewData)

  const [atRecords, setAtRecords, atRecordsRef] = useStateWithRef<
    { id: string; userId: string; name: string }[]
  >([])
  const atList = useMemo(
    () => [...new Set(atRecords.map(r => ({ userId: r.userId, nickname: r.name })))],
    [atRecords],
  )
  useEffect(() => setAtList(atList), [atList, setAtList])

  // 提交 @ 内容，把当前 @ 文本替换为特殊的 @ 元素
  const addAt = useCallback(
    (member: AtMember) => {
      const newRecord = { id: Date.now().toString(), userId: member.userId, name: member.name }
      setAtRecords([...atRecordsRef.current, newRecord])

      const atElement = makeAtElement(newRecord.id, member.name, nodeClassName)
      const viewData = viewDataRef.current
      if (viewData) {
        closeView()

        // 把文本框拆成 “@ 内容前面” 和 “@内容后面” 两部分（抛除 @ 内容）
        // 然后把 @ 元素插入到它们两个中间
        const { node, offset, search } = viewData
        const fragment = makeFragment(
          node.data.slice(0, offset - search.length - 1),
          atElement,
          node.data.slice(offset),
        )
        node.parentElement?.replaceChild(fragment, node)
      } else {
        // 没有 viewData 说明是外部触发的 @ 行为
        const input = inputRef.current
        if (input) input.appendChild(atElement)
      }

      // 移除之前建立的 placeholder span（见下一个段落）
      const input = inputRef.current
      if (input) {
        input.querySelectorAll('[data-placeholder]').forEach(placeholder => {
          const fragment = document.createDocumentFragment()
          ;[...placeholder.childNodes].forEach(node => fragment.appendChild(node)) // 这里必须转成数组，不然 forEach 里的 appendChild 操作会导致部分 child 遍历不到
          placeholder.parentElement?.replaceChild(fragment, placeholder)
        })
      }

      // @ 元素处于末尾时，在其后补充一个 placeholder span，不然会看不到光标
      const next = atElement.nextSibling
      if (!next || (next instanceof Node && next.nodeValue === '')) {
        const placeholder = document.createElement('span')
        placeholder.dataset.placeholder = '1'
        atElement.parentElement?.appendChild(placeholder)
      }

      // 把光标定位到 @ 元素后面
      selectAfter(atElement)
    },
    [nodeClassName, closeView, inputRef],
  )

  // @ 内容被删除时，移除对应的 at record
  const atRemoved = useCallback(
    (recordId: string) => {
      setAtRecords(atRecordsRef.current.filter(r => r.id !== recordId))
    },
    [setAtRecords, atRecordsRef],
  )

  useEffect(() => {
    if (!inputRef.current) return
    const observer = new MutationObserver(mutationList => {
      for (const mutation of mutationList) {
        for (const node of mutation.removedNodes) {
          if (node instanceof HTMLElement && truthy(node.dataset.recordId))
            atRemoved(node.dataset.recordId)
        }
      }
    })
    observer.observe(inputRef.current, { childList: true })
    return () => observer.disconnect()
  }, [inputRef, atRecordsRef, atRemoved])

  // 响应外部触发的 @ 行为
  useObservable(atEvent, member => {
    addAt(member)
  })

  return {
    addAt,
  }
}

function makeAtElement(recordId: string, memberName: string, className: string) {
  const element = document.createElement('span')
  element.innerText = ` @${memberName} `
  element.className = className
  element.style.userSelect = 'none'
  element.contentEditable = 'false'
  element.dataset.recordId = recordId
  element.addEventListener('click', () => selectAfter(element))
  return element
}

function makeFragment(...items: (string | Element)[]) {
  const fragment = document.createDocumentFragment()
  for (const item of items) {
    if (typeof item === 'string') fragment.appendChild(document.createTextNode(item))
    else fragment.appendChild(item)
  }
  return fragment
}

function selectAfter(node: Node) {
  const selection = getSelection()
  if (selection) {
    const range = document.createRange()
    range.setStartAfter(node)
    range.collapse(true)
    selection.removeAllRanges()
    selection.addRange(range)
  }
}
